package com.study.easyexcel.api;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.builder.ExcelWriterSheetBuilder;
import com.alibaba.excel.write.handler.CellWriteHandler;
import com.alibaba.excel.write.handler.WriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.excel.write.metadata.fill.FillWrapper;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.study.easyexcel.api.handler.DropdownBoxHandler;
import com.study.easyexcel.api.handler.HyperlinkHandler;
import com.study.easyexcel.common.constant.ExcelConstants;
import com.study.easyexcel.common.domain.*;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Workbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ObjectUtils;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Excel导入导出工具
 *
 * @author yuhuan
 * @date 2023/5/8
 */
public class ExcelUtils {

    private static final Logger logger = LoggerFactory.getLogger(ExcelUtils.class);

    /**
     * 填充Excel，允许填充多个工作溥，但所有工作溥共享同一个列表数据
     *
     * @param templateFile 要填充的模板文件
     * @param excelName    要导出的excel文件名称
     * @param sheets       Excel工作溥，每个{@link TemplateSheet}表示一个工作薄
     * @param response     使用response可以导出到浏览器
     */
    public static <T> void shareFillingExcel(InputStream templateFile, String excelName, List<SingleTemplate<T>> sheets, HttpServletResponse response) {
        try {
            ExcelWriter excelWriter = EasyExcel.write(downLoad(excelName, response)).withTemplate(templateFile).build();
            for (SingleTemplate<T> es : sheets) {
                WriteSheet sheet = EasyExcel.writerSheet(es.getSheetName()).build();
                // 在写入list的时候不管list下面有没有空行 都会创建一行，然后下面的数据往后移动
                // 所有的数据都放到内存了，所以慎用；如果数据量太大可以在列表写入完成后想办法手动写入表尾的数据
                FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
                excelWriter.fill(es.getMetaData(), sheet);
                excelWriter.fill(es.getMetaList(), fillConfig, sheet);
            }
            excelWriter.finish();
        } catch (IOException e) {
            logger.error("文件导出失败 msg={}", e.getMessage());
        }
    }

    /**
     * 填充Excel，导出更复杂的表格。允许不同的列表填充到同一个工作溥，并且开启了公式的强制刷新，填充数据请
     * 不要轻易使用等于号
     *
     * @param templateFile 要填充的模板文件
     * @param excelName    要导出的excel文件名称
     * @param sheetData    Excel工作溥填充的数据
     * @param response     使用response可以导出到浏览器
     */
    public static <T> void mergeFillingExcel(InputStream templateFile, String excelName, GroupTemplate<Map<String, Object>> sheetData, HttpServletResponse response) {
        try {
            // 准备写入
            ExcelWriter excelWriter = EasyExcel.write(downLoad(excelName, response)).withTemplate(templateFile).build();
            ExcelWriterSheetBuilder excelWriterSheetBuilder = EasyExcel.writerSheet(sheetData.getSheetName());
            // 注册处理器，不同数据组的样式处理器直接覆盖上一个数据组的样式
            // 此处一定要确保处理器不为空，否则会注册空处理器从而导致填充时发生空指针异常
            List<CellWriteHandler> writeHandlers = sheetData.getAllWriteHandler();
            if (!ObjectUtils.isEmpty(sheetData.getHandler())) {
                writeHandlers.add(sheetData.getHandler());
            }
            for (CellWriteHandler writeHandler : writeHandlers) {
                excelWriterSheetBuilder = excelWriterSheetBuilder.registerWriteHandler(writeHandler);
            }
            WriteSheet writeSheet = excelWriterSheetBuilder.build();
            FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
            // 填充列表属性
            // 必须保证填充顺序，否则会出现样式紊乱（可通过反排序复现异常情况）
            List<ListBodyStyleMap<Map<String, Object>>> bodyStyleMaps = sheetData.bodySorted().getListMap();
            for (ListBodyStyleMap<Map<String, Object>> bodyStyleMap : bodyStyleMaps) {
                FillWrapper fillWrapper = new FillWrapper(bodyStyleMap.getListPrefix(), bodyStyleMap.getDataBody());
                excelWriter.fill(fillWrapper, fillConfig, writeSheet);
            }
            // 设置强制计算公式：不然公式会以字符串的形式显示在excel中
            Workbook workbook = excelWriter.writeContext().writeWorkbookHolder().getWorkbook();
            workbook.setForceFormulaRecalculation(true);
            // 填充普通属性
            excelWriter.fill(sheetData.getMetaData(), writeSheet);
            excelWriter.finish();
        } catch (IOException e) {
            logger.error("文件导出失败 msg={}", e.getMessage());
        }
    }

    /**
     * 导出Excel，不固定表头和列数；表头来源于类型{@link List}
     *
     * @param excelName 要导出的excel名称
     * @param sheet     工作溥
     * @param response  使用response可以导出到浏览器
     */
    public static void exportDynamicColumn(String excelName, DynamicSheet sheet, HttpServletResponse response) {
        try {
            // 设置头策略
            WriteCellStyle headWriteCellStyle = new WriteCellStyle();
            // 背景色
            headWriteCellStyle.setFillForegroundColor(IndexedColors.SKY_BLUE.getIndex());
            // 字体
            WriteFont headWriteFont = new WriteFont();
            headWriteFont.setFontHeightInPoints((short) 14);
            headWriteCellStyle.setWriteFont(headWriteFont);
            // 设置内容策略
            WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
            WriteFont contentWriteFont = new WriteFont();
            // 字体大小
            contentWriteFont.setFontHeightInPoints((short) 11);
            contentWriteCellStyle.setWriteFont(contentWriteFont);
            // 内置样式策略
            HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
            // 将数据写入指定工作薄
            EasyExcel.write(downLoad(sheet.getSheetName(), response)).registerWriteHandler(horizontalCellStyleStrategy).head(sheet.getDynamicHead()).sheet(excelName).doWrite(sheet.getList());
        } catch (IOException e) {
            logger.error("文件导出失败 msg={}", e.getMessage());
        }
    }

    /**
     * 导出Excel，包含指定列；表头来源于{@link Object}
     * {@link ExcelWriterBuilder#includeColumnFiledNames(Collection)}   导出指定的列
     *
     * @param excelName 要导出的excel名称
     * @param sheet     工作溥
     * @param response  使用response可以导出到浏览器
     */
    public static <T> void exportIncludeColumn(String excelName, ExcelSheet<T> sheet, HttpServletResponse response) {
        try {
            // 将数据写入指定工作薄
            EasyExcel.write(downLoad(excelName, response), sheet.getPojoClass()).includeColumnFiledNames(sheet.getIncludeColumnFiledNames()).sheet(sheet.getSheetName()).doWrite(sheet.getMateList());
        } catch (IOException e) {
            logger.error("文件导出失败 msg={}", e.getMessage());
        }
    }

    /**
     * 导出Excel，排除指定列；表头来源于{@link Object}
     * {@link ExcelWriterBuilder#excludeColumnFiledNames(Collection)}   排除指定的列
     *
     * @param excelName 要导出的excel名称
     * @param sheet     工作溥
     * @param response  使用response可以导出到浏览器
     * @param <T>
     */
    public static <T> void exportExcludeColumn(String excelName, ExcelSheet<T> sheet, HttpServletResponse response) {
        try {
            // 将数据写入指定工作薄
            EasyExcel.write(downLoad(excelName, response), sheet.getPojoClass()).excludeColumnFiledNames(sheet.getExcludeColumnFiledNames()).sheet(sheet.getSheetName()).doWrite(sheet.getMateList());
        } catch (IOException e) {
            logger.error("文件导出失败 msg={}", e.getMessage());
        }
    }

    /**
     * 导出数据到Excel；表头来源于{@link Object}
     *
     * @param excelName 要导出的excel名称
     * @param sheet     工作溥
     * @param response  使用response可以导出到浏览器
     * @param <T>
     */
    public static <T extends AdditionalImage> void downLoadExcel(String excelName, ExcelSheet<T> sheet, HttpServletResponse response) {
        List<T> mateList = sheet.getMateList();
        try {
            // 将数据写入指定工作薄
            EasyExcel.write(downLoad(excelName, response), sheet.getPojoClass())
                    .registerWriteHandler(DropdownBoxHandler.buildScan(sheet.getPojoClass()).lastRow(mateList.size()))
                    .sheet(sheet.getSheetName())
                    .doWrite(mateList);
        } catch (IOException e) {
            logger.error("文件导出失败 msg={}", e.getMessage());
        }
    }

    /**
     * 导出Excel模板；自由度更高的写入方式，使用不同的数据对象写入一个文件不同的工作薄
     * {@link ExcelWriterBuilder#needHead(Boolean)} 是否需要表头
     * {@link ExcelWriterBuilder#head(Class)}       读取文件头对应的列表，会根据列表匹配数据
     * {@link ExcelWriterBuilder#registerWriteHandler(WriteHandler)} 注册拦截器
     *
     * @param excelName 要导出的excel名称
     * @param docList   说明书
     * @param sheet     工作溥
     * @param response  导出到浏览器
     * @param <T>       模板的说明书类型
     */
    public static <T extends Instructions, U> void exportTemplate(String excelName, List<T> docList, ExcelSheet<U> sheet, HttpServletResponse response) {
        try {
            OutputStream outputStream = downLoad(excelName, response);
            // 指定文件
            ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
            // 预写入说明书，无表头工作薄
            excelWriter.write(docList, EasyExcel.writerSheet("说明").head(Instructions.class).needHead(Boolean.FALSE).build());
            // 预写入模板工作薄（数据导入模板）
            WriteSheet writeSheet = EasyExcel.writerSheet(sheet.getSheetName())
                    .excludeColumnFiledNames(sheet.getExcludeColumnFiledNames())
                    .registerConverter(sheet.getConverter())
                    .registerWriteHandler(DropdownBoxHandler.build().explicitList(sheet.getDropdownBoxList()).scan(sheet.getPojoClass()))
                    .registerWriteHandler(HyperlinkHandler.build().scan(sheet.getPojoClass()))
                    .head(sheet.getPojoClass()).build();
            excelWriter.write(sheet.getMateList(), writeSheet);
            // 将所有工作薄写入文件
            excelWriter.finish();
        } catch (IOException e) {
            logger.error("模板导出失败 msg={}", e.getMessage());
        }
    }

    /**
     * 导出动态模板
     *
     * @param excelName 要导出的excel名称
     * @param docList   说明书
     * @param sheet     工作溥
     * @param response  使用response可以导出到浏览器
     * @param <T>       模板的说明书类型
     */
    public static <T extends Instructions> void exportDynamicTemplate(String excelName, List<T> docList, DynamicSheet sheet, HttpServletResponse response) {
        try {
            OutputStream outputStream = downLoad(excelName, response);
            ExcelWriter excelWriter = EasyExcel.write(outputStream).build();
            // 特殊的工作溥（填写说明）
            excelWriter.write(docList, EasyExcel.writerSheet("说明").head(Instructions.class).needHead(Boolean.FALSE).build());
            // 填写数据的主工作溥
            WriteSheet writeSheet = EasyExcel.writerSheet(excelName).head(sheet.getDynamicHead()).build();
            excelWriter.write(sheet.getList(), writeSheet);
            excelWriter.finish();
        } catch (IOException e) {
            logger.error("模板导出失败 msg={}", e.getMessage());
        }
    }

    /**
     * 下载到浏览器
     *
     * @param excelName 要下载的文件名
     * @param response  使用{@link HttpServletResponse}可以下载到浏览器
     * @throws IOException 输出流获取异常
     */
    public static OutputStream downLoad(String excelName, HttpServletResponse response) throws IOException {
        response.setContentType(ExcelConstants.EXCEL_CONTENT_TYPE);
        response.setCharacterEncoding(StandardCharsets.UTF_8.name());
        logger.info("下载<{}{}>", excelName, ExcelTypeEnum.XLSX.getValue());
        // 这里URLEncoder.encode可以防止中文乱码
        String fileName = URLEncoder.encode(excelName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
        response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ExcelTypeEnum.XLSX.getValue());
        // 将数据写入指定工作薄
        return response.getOutputStream();
    }
}
